<?php

/**
 * QPHP
 *
 * Copyright (c) 2022 QPHP
 *
 * @author lbnnbs
 * @copyright Copyright (c) 2022 lbnnbs QPHP (https://gitee.com/lbnnbs/qphp)
 * @license AGPLv3 License
 * 
 * 文件上传
 * 
 * @example
 * 
 * [示例如下]
 * // 生成对象
 * $up = new \Uploader('file1');
 * if(!$up->hasUpload()){
 *     echo '没有文件上传';
 *     return;
 * }
 * 
 * // 设置属性
 * $up->set(array('diskPath'=>'./img/'))->set(array('isThumb'=>true,'thumb'=>array(array(50,50),array(100,100))))->upload();
 * if($up->hasError()){
 *     echo $up->error();
 * }else{
 *     echo '上传的文件名：'.$up->getUploadFile().'<br/>';
 *     echo '二个缩略图文件名:';
 *     dump($up->getThumbFile());
 * }
 * ---------------------------------------------------------
 * 
 * [注意]
 * 
 * 1:不支持 FILE为数组的方式
 * <input type="file" name="pfile[]" />
 * <input type="file" name="pfile[]" />
 * 
 * 2:form表单需要指定 enctype="multipart/form-data"
 */
class Uploader {

    private $_error_msg = '';

    /**
     * 文件类型
     *
     * @var array
     */
    private $_fileType = array(
        'other' => array('application/octet-stream'),
        'bmp' => array('image/bmp'),
        'gif' => array('image/gif'),
        'txt' => array('text/plain'),
        'jpg' => array('image/jpg', 'image/jpe', 'image/jpeg', 'image/pjpeg'),
        'jpeg' => array('image/jpg', 'image/jpe', 'image/jpeg', 'image/pjpeg'),
        'png' => array('image/x-png', 'image/png'),
        'doc' => array('application/msword'),
        'docx' => array('application/vnd.openxmlformats-officedocument.wordprocessingml.document'),
        'xls' => array('application/vnd.ms-excel'),
        'xlsx' => array('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'),
        'swf' => array('application/x-shockwave-flash'),
        'pdf' => array('application/pdf'),
        'pem' => array('application/octet-stream'),
    );

    /**
     * 属性设置
     *
     * @var array
     */
    private $_sets = array
        (
        // 可上传文件的类型,如果想上传更安全的文件(防止修改文件扩展名)； 如果想上传所有文件请设置为 "*"
        'type' => 'jpg|gif|png|bmp|doc|docx|xls|xlsx',
        // 允许的最大文件大小,单位:KB  0:表示任何大小
        'maxSize' => 10240, // 10 * 1024
        // 上传文件后保存的目录(要可写)，默认/uploads/
        'diskPath' => '',
        // 上传文件后保存的网站前缀，默认/uploads/
        'urlPath' => '',
        // 当目标文件存在时是否删除
        'isRemove' => false,
        // 是否为生成缩略图
        'isThumb' => false,
        /**
         * 生成缩略图的尺寸,支持多组尺寸(即同时生成多张缩略图)定义.格式如下:
         * 注意是两层数组
         * array(array(50,40), array(100,80));
         *
         * 说明:
         *
         * 1:生成二组缩略图
         * 第一组尺寸是: 50:缩略图最大宽度 40:缩略图最大高度
         * 第二组尺寸是: 100:缩略图最大宽度 80:缩略图最大高度
         *
         * 2:生成的缩略图文件名
         * 格式为: "[上传后的文件名(包含扩展名)].[缩略图宽]_[缩略图高].扩展名]"
         * 例如: 文件上传后的文件名为:"xxxx.jpg" 则第一组尺寸生成后的缩略图文件名为:"xxxx.jpg.50_40.jpg"
         */
        'thumb' => array(),
        /**
         * 压缩到指定大小
         */
        'compress' => array(750, 1200),
        /**
         * 是否按日期分开存储
         */
        'separate_save' => true,
    );

    /**
     * 属性信息
     *
     * @var array
     */
    private $_options = array
        (
        // 错误序号
        'errorNo' => 0,
        // 错误定义
        'errorMsg' => array(
            -1 => '没有指定FILE表单域',
            -2 => '没有文件上传',
            -3 => '文件超过大小限制',
            -4 => '不是指定的类型',
            -5 => '目标文件已存在',
            -6 => '目标文件不能写入',
            -7 => '生成缩略图失败',
            -8 => '压缩图片失败',
        ),
        // 生成的缩略图文件名
        'thumbFiles' => array(),
        // FILE控件名称
        'inputName' => '',
        // 上传文件后保存的文件名
        'fileName' => '',
    );

    /**
     * 构造函数
     *
     * @param string $inputName: 表单中FILE控件名称,如 <input type="file" name="upfile"> 中的"upfile"
     */
    public function __construct($inputName = '') {
        $this->_options['inputName'] = $inputName;
        $this->set(array(
            'diskPath' => UPLOAD_PATH,
            'urlPath' => UPLOAD_URL,
        ));
    }

    /**
     * 判断当前表单是否有文件上传了
     *
     * @return bool
     */
    public function hasUpload() {
        return isset($_FILES[$this->_options['inputName']]) && $_FILES[$this->_options['inputName']]['name'] != '';
    }

    /**
     * 返回文件存储磁盘根目录
     * @return string
     */
    public function getBasePath() {
        return $this->_sets['diskPath'];
    }

    /**
     * 返回文件存储网址根目录
     * @return string
     */
    public function getBaseUrl() {
        return $this->_sets['urlPath'];
    }

    /**
     * 设置 FILE控件名称
     *
     * @param string $inputName:FILE域名称
     */
    public function setInputName($inputName) {
        $this->_options['inputName'] = $inputName;
        return $this;
    }

    /**
     * 设置上传属性
     *
     * @param array $sets:属性设置
     */
    public function set($sets) {
        // 如果保存的目录没有以 '/' 结束则要加上它
        if (isset($sets['diskPath']) && substr($sets['diskPath'], -1, 1) != '/') {
            $sets['diskPath'] .= '/';
        }
        if (isset($sets['urlPath']) && substr($sets['urlPath'], -1, 1) != '/') {
            $sets['urlPath'] .= '/';
        }
        $this->_sets = array_merge($this->_sets, $sets);
        return $this;
    }

    private function _mkDirs($dir) {
        if (!is_dir($dir)) {
            if (!$this->_mkDirs(dirname($dir))) {
                return false;
            }
            if (!mkdir($dir, 0777)) {
                return false;
            }
        }
        return $dir;
    }

    /**
     * 上传文件动作
     *
     * 说明:$dstFileName 有以下三种处理情况
     *
     * 1:为空时,系统自动生成随机唯一的文件名;其扩展名与上传时所选择的文件一致.
     * 2:没有指定扩展名时如 "fileName".则系统自动加上上传时所选择文件的扩展名
     * 3:指定了完整的文件名如 "fileName.jpg" 则完全采用自定义的文件名
     *
     * @param string $dstFileName 目标文件名
     * @return bool
     */
    public function upload($dstFileName = '') {
        // 如果文件域不存在
        if ($this->_options['inputName'] == '' || !isset($_FILES[$this->_options['inputName']])) {
            $this->_options['errorNo'] = -1;
            return false;
        }

        // 生成目标文件文件
        $this->_makeFileName($dstFileName);

        // 是否为可上传的文件
        $tmpFile = $_FILES[$this->_options['inputName']]['tmp_name'];
        if ($tmpFile == '' || !is_uploaded_file($tmpFile)) {
            $this->_options['errorNo'] = -2;
            return false;
        }

        // 文件大小是否超过了设置
        if ($this->_sets['maxSize'] > 0) {
            if ($_FILES[$this->_options['inputName']]['size'] > ($this->_sets['maxSize'] * 1024)) {
                $this->_options['errorNo'] = -3;
                return false;
            }
        }

        // 文件是否是指定的类型
        if ($this->_sets['type'] != '*') {
            // 得到所有允许的类型
            $typeArr = array();
            $ftypes = explode('|', $this->_sets['type']);
            foreach ($ftypes as $ft) {
                $typeArr = array_merge($typeArr, $this->_fileType[$ft]);
            }
            // 是否存在
            $ut = trim($_FILES[$this->_options['inputName']]['type']);
            if (!in_array($ut, $typeArr)) {
                $this->_options['errorNo'] = -4;
                return false;
            }
            $size = getimagesize($_FILES[$this->_options['inputName']]['tmp_name']);
            $ut = $size['mime'];
            if ($ut && !in_array($ut, $typeArr)) {
                $this->_options['errorNo'] = -4;
                return false;
            }
        }

        // 目标文件是否存在
        $dstfile = $this->_sets['diskPath'] . $this->_options['fileName'];
        if (file_exists($dstfile)) {
            if ($this->_sets['isRemove']) {
                @unlink($dstfile);
            } else {
                $this->_options['errorNo'] = -5;
                return false;
            }
        }

        // 复制文件
        if (@copy($tmpFile, $dstfile)) {
            @chmod($dstfile, 0777);
        } else {
            $this->_options['errorNo'] = -6;
            return false;
        }

        // 如果生成缩略图
        if ($this->_sets['compress']) {
            $this->_compress();
        }

        // 如果生成缩略图
        if ($this->_sets['isThumb']) {
            $this->_makethumb();
        }
        return true;
    }

    /**
     * 得到上传后的文件名
     *
     * @param bool $path: true:包括全路径 false:不带路径
     * @return string
     */
    public function getUploadFile($path = false) {
        // 如果有错误
        if ($this->hasError()) {
            return false;
        }
        return $path ? $this->_sets['diskPath'] . $this->_options['fileName'] : $this->_sets['urlPath'] . $this->_options['fileName'];
    }

    /**
     * 得到所有缩略图的文件名
     *
     * @return array
     */
    public function getThumbFile() {
        return $this->_options['thumbFiles'];
    }

    /**
     * 判断是否出错了
     */
    public function hasError() {
        return $this->_options['errorNo'] != 0;
    }

    /**
     * 返回错误信息
     *
     * @return string
     */
    public function error() {
        return $this->hasError() ? $this->_options['errorMsg'][$this->_options['errorNo']] : '';
    }

    /**
     * 生成文件名
     */
    private function _makeFileName($dstFileName) {
        if ($dstFileName == '') {
            if ($this->_sets['separate_save']) {
                $folder = date('Y/m/d');
                $this->_mkDirs($this->_sets['diskPath'] . $folder);
                $this->_options['fileName'] = $folder . '/' . $this->_randFileName() . $this->_fileExt($_FILES[$this->_options['inputName']]['name']);
            } else {
                $this->_options['fileName'] = $this->_randFileName() . $this->_fileExt($_FILES[$this->_options['inputName']]['name']);
            }
        } else {

            if (!strpos($dstFileName, '.'))
                $this->_options['fileName'] = $dstFileName . $this->_fileExt($_FILES[$this->_options['inputName']]['name']);
            else
                $this->_options['fileName'] = $dstFileName;
        }
    }

    /**
     * 得到随机唯一的文件名
     */
    private function _randFileName() {
        return md5(uniqid(time() . rand()));
    }

    /**
     * 得到小写的文件扩展名,如".php"
     *
     * @param string $fileName:文件名
     * @return string
     */
    private function _fileExt($fileName) {
        return strtolower(strrchr($fileName, "."));
    }

    public function getErrorMsg() {
        return $this->_error_msg;
    }

    /**
     * 生成缩略图
     *
     */
    private function _makethumb() {
        $srcfile = $this->getUploadFile(true);
        foreach ($this->_sets['thumb'] as $row) {
            @list($thumbWidth, $thumbHeight) = $row;
            $dstfile = $srcfile . '.' . $thumbWidth . '_' . $thumbHeight . fileExt($srcfile);
            if (Image::thumbImg($srcfile, $dstfile, $thumbWidth, $thumbHeight)) {
                $this->_options['thumbFiles'][] = $dstfile;
            } else {
                $this->_options['errorNo'] = -7;
            }
        }
    }

    private function _compress() {

        // 是否是图片类型
        $typeArr = array();
        $ftypes = array('jpeg', 'jpg', 'png', 'gif');
        foreach ($ftypes as $ft) {
            $typeArr = array_merge($typeArr, $this->_fileType[$ft]);
        }

        $ut = trim($_FILES[$this->_options['inputName']]['type']);

        if (in_array($ut, $typeArr)) {
            $srcfile = $this->getUploadFile(true);

            if (!Image::thumbImg($srcfile, $srcfile, $this->_sets['compress'][0], $this->_sets['compress'][1])) {
                $this->_options['errorNo'] = -8;
            }
        }
    }

}
